Skip to content

fix: propagate proxy settings to SSH environment#1012

Open
EhabY wants to merge 3 commits into
mainfrom
fix/eng-2919-proxycommand-proxy-env
Open

fix: propagate proxy settings to SSH environment#1012
EhabY wants to merge 3 commits into
mainfrom
fix/eng-2919-proxycommand-proxy-env

Conversation

@EhabY

@EhabY EhabY commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • propagate VS Code proxy settings into process.env before Remote-SSH starts the local SSH/ProxyCommand flow
  • set both HTTP_PROXY and HTTPS_PROXY from http.proxy when it applies to the Coder deployment URL
  • set NO_PROXY from coder.proxyBypass, falling back to http.noProxy
  • watch proxy settings so users are prompted to reload when SSH proxy behavior changes
  • surface a dismissable warning (with a one-click "Enable Local Server") when a proxy applies but remote.SSH.useLocalServer is disabled, since propagation can't reach the connection in that case

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test:extension ./test/unit/api/proxy.test.ts ./test/unit/api/utils.test.ts ./test/unit/remote/environment.test.ts ./test/unit/util/notifications.test.ts

Manual testing matrix

Verified that the proxy variables actually reach the spawned coder ssh ProxyCommand process by injecting a marker variable into the extension-host process.env and reading the live ProxyCommand process's environment (/proc/<pid>/environ) once connected.

Editor Remote-SSH Inherited?
VS Code (MS) useLocalServer=true
VS Code (MS) useLocalServer=false ❌ not propagated
Cursor default
Antigravity default
Devin (formerly Windsurf) default
VSCodium + open-source Remote-SSH default

Notes:

  • The only failing case is MS VS Code with useLocalServer=false, which spawns ssh off a path that does not inherit the extension host's process.env. useExecServer made no difference either way.
  • The VS Code clones all take the local-server-style spawn path, so they inherit correctly out of the box.

Delivery options considered

The coder CLI has no proxy flag and reads HTTP_PROXY/HTTPS_PROXY/NO_PROXY from its own process environment (standard Go HTTP client), so the goal is getting those variables into the coder ssh ProxyCommand process. Three approaches:

  • A. Mutate process.env (chosen) — spawned ssh children inherit it.
  • B. Inline on the ProxyCommand line in the SSH config (HTTP_PROXY=... coder ssh ...).
  • C. Generated wrapper script the ProxyCommand calls, which exports the vars before exec'ing coder.

We went with A because it is the least intrusive (no file writes) and handles multiple windows onto the same connection cleanly: B and C bake a single static value into a file shared across windows, whereas A keeps values in memory and per-window, leaving room for per-window variables later (e.g. a SESSION_ID). The tradeoff is that A is best-effort for the useLocalServer=false case noted above, which is why we additionally surface the dismissable warning for that combination.

Closes #1010

@linear-code

linear-code Bot commented Jun 17, 2026

Copy link
Copy Markdown

ENG-2919

@EhabY EhabY self-assigned this Jun 17, 2026
@EhabY EhabY force-pushed the fix/eng-2919-proxycommand-proxy-env branch from 0d111a7 to c830811 Compare June 17, 2026 15:06
EhabY added 2 commits June 18, 2026 13:20
Set HTTP_PROXY/HTTPS_PROXY/NO_PROXY on the extension host's process.env
from VS Code's http.proxy / coder.proxyBypass / http.noProxy so the
spawned `coder ssh` ProxyCommand inherits them. Mutating process.env
avoids writing a credentialed proxy URL to the SSH config and keeps
multiple windows onto the same workspace independent.

Watch the proxy settings so the user is prompted to reload when SSH
proxy behavior changes.
When a proxy applies to the deployment but remote.SSH.useLocalServer is off,
the spawned SSH connection won't inherit the proxy env. Surface a dismissable
warning offering to enable the local server, via a reusable
showDismissibleNotification helper.
@EhabY EhabY force-pushed the fix/eng-2919-proxycommand-proxy-env branch from c830811 to d4ed808 Compare June 18, 2026 10:21
Move showDismissibleNotification from util into a DismissibleNotifier
class under core: it holds globalState and constrains keys to a known
DismissibleNotificationKey set, wired through ServiceContainer.

Rename applySshProxyEnvironment to applySshEnvironment, generalize its
doc to "SSH-related environment variables", keep the internal env type
unexported, and order the file public-API first.

Make the useLocalServer warning a blocking warning modal so the setting
is written (via the jsonc settings util, not cfg.update) before ssh
spawns, which lets it apply without a window reload.
@EhabY EhabY force-pushed the fix/eng-2919-proxycommand-proxy-env branch from db5ca20 to da8f590 Compare June 18, 2026 13:08
@EhabY EhabY requested a review from code-asher June 22, 2026 13:40
Comment on lines +8 to +13
export const DISMISSIBLE_NOTIFICATION_KEYS = [
"coder.proxyUseLocalServerWarningDismissed",
] as const;

export type DismissibleNotificationKey =
(typeof DISMISSIBLE_NOTIFICATION_KEYS)[number];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not enum? This looks basically like an enum.

Or, if the idea is that we want to pass string literals then we could just do type DISMISSIBLE_NOTIFICATION_KEYS = "key" | "key2" since it does not look like we use the const for anything right now.

But if we will need to iterate over it or something like that in the future, then ignore me.

Comment thread src/remote/environment.ts
Comment on lines +32 to +35
* Best-effort: only processes spawned afterwards inherit the change. MS VS Code
* with `remote.SSH.useLocalServer=false` spawns ssh off a path that does not
* inherit, so propagation there needs `useLocalServer=true`. Returns a disposable
* that restores the previous values.

@code-asher code-asher Jun 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the location of the SSH binary change whether environment variables are inherited? Or by path do you mean the code path and not the binary path?

If I understand correctly, true spawns a single SSH process, and then VS Code multiplexes all windows connected to that remote using that single SSH connection.

And then false spawns SSH in a hidden terminal per window. And I guess when they do it this way, they are probably explicitly setting env which prevents inheriting as a side effect?

Comment thread src/remote/environment.ts
Comment on lines +55 to +56
const proxy = httpProxy
? getProxyForUrl(baseUrl, httpProxy, noProxy, undefined)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we call this differently here than in the plugin? We could just pass the fallback as the fourth option to get the same behavior, right?

I think maybe it is so we can use noProxy below but this seems a bit wrong because getProxyForUrl also checks other variables that were not included here (like npm_config_no_proxy). Maybe getProxyForUrl could return both proxy and noProxy?

Actually, there is no need to even set noProxy right? Because getProxyForUrl will return a blank string if any noProxy is in effect.

Or, maybe better to skip getProxyForUrl and just pass everything in and let the CLI figure it out. For example maybe the CLI tries to use a different region rather than the main deployment domain or something.

Comment thread src/remote/environment.ts

function getEnvKeys(env: Environment, key: string): string[] {
const keys = Object.keys(env).filter(
(envKey) => envKey.toLowerCase() === key.toLowerCase(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment variables are case-sensitive (at least, in Linux, Windows might be different) so I think we should not compare with lowercase or we technically cause collisions.

Comment thread src/remote/environment.ts
return;
}
disposed = true;
for (let i = previous.length - 1; i >= 0; i--) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not using a for of like above because it needs to iterate backwards? The keys are unique though so I am not sure why the order matters.

Comment thread src/remote/environment.ts
: "";

return {
...(proxy ? { HTTP_PROXY: proxy, HTTPS_PROXY: proxy } : {}),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it right to set both http_proxy and https_proxy? If the user set http_proxy I imagine we should not set it on https_proxy, and maybe vice versa. idk why someone would have Coder on an http URL though, other than for maybe testing.

Comment thread src/remote/environment.ts
baseUrl: string,
cfg: Pick<WorkspaceConfiguration, "get">,
): SshEnvironment {
const httpProxy = getSetting(cfg, "http.proxy");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nbd but could just do cfg.get right? A blank or even untrimmed string with spaces would not break anything here (and we are not doing it elsewhere when we get the proxy URL, so if it does break we should do it there as well).

Comment thread src/remote/remote.ts
const ENABLE = "Enable Local Server";
const choice = await this.dismissibleNotifier.showDismissible(
"coder.proxyUseLocalServerWarningDismissed",
"Your proxy settings may not reach the SSH connection because `remote.SSH.useLocalServer` is disabled. Enable it so Coder can apply the proxy to the connection.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we validated this? I wonder if VS Code might actually explicitly include http_proxy when spawning, it does seem reasonable. But maybe not.

Comment thread src/remote/remote.ts
Comment on lines +870 to +879
// Use the jsonc writer, not cfg.update which can hang during remote setup.
// No reload needed: ssh hasn't spawned yet, so it picks this up on connect.
const ok = await applySettingOverrides(
this.pathResolver.getUserSettingsPath(),
[{ key: "remote.SSH.useLocalServer", value: true }],
this.logger,
);
if (!ok) {
this.logger.warn("Failed to enable remote.SSH.useLocalServer");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the setting through the API or does it only let us change our own settings? I think I remember trying it through the API before but it was not possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProxyCommand for the SSH tunnel does not receive http.proxy

2 participants